package crmdna.client.program;

import static crmdna.common.OfyService.ofy;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.googlecode.objectify.Work;

import crmdna.client.counter.Counter;
import crmdna.client.counter.Counter.CounterType;
import crmdna.client.member.Member;
import crmdna.client.program.Program.ProgramProp;
import crmdna.client.user.User;
import crmdna.client.user.User.Action;
import crmdna.client.user.User.ResourceType;
import crmdna.common.APIException;
import crmdna.common.APIResponse.Status;
import crmdna.common.Utils;
import crmdna.crmdna.Client;

public class CheckIn {
	public static class CheckInProp {
		String key; //programId_memberId
		long programId;
		long memberId;
		
		List<CheckInRecord> checkInRecords = new ArrayList<>();
		
		CheckInRecord getCheckInRecord(int sessionDateYYYYMMDD) {
			for (int i = checkInRecords.size() - 1; i >= 0; i--) {
				CheckInRecord checkInOrOut = checkInRecords.get(i);
				if (checkInOrOut.sessionDateYYYYMMDD == sessionDateYYYYMMDD)
					return checkInOrOut;			
			}
			
			return null;
		}
		
		int getNumCheckins() {
			Map<Integer, Boolean> map = new HashMap<>();
			
			for (CheckInRecord checkInRecord : checkInRecords) {
				if (checkInRecord.isCheckin) {
					map.put(checkInRecord.sessionDateYYYYMMDD, true);
				}
			}
			
			int numCheckins = 0;
			for (Integer date : map.keySet()) {
				if (map.get(date) == true)
					numCheckins++;
			}
			
			return numCheckins;
		}				
	}
	
	public static class CheckInStatusProp {
		public boolean checkedIn;
		public String userFriendlyMessage;
	}			
	
	public static List<CheckInStatusProp> getCheckInStatus(String client, long programId,
			List<Long> memberIds, 
			int sessionDateYYYYMMDD) {
		
		Client.ensureValidClient(client);
		
		List<CheckInStatusProp> result = new ArrayList<>();
		//result will have the same size as memberIds
		List<String> checkInKeys = new ArrayList<>();
		
		for (int i = 0; i < memberIds.size(); i++) {
			CheckInStatusProp checkInStatusProp = new CheckInStatusProp();
			checkInStatusProp.checkedIn = false;
			checkInStatusProp.userFriendlyMessage = "Not checked in";
			result.add(checkInStatusProp);
			checkInKeys.add(getCheckInKey(programId, memberIds.get(i)));
		}
											
		Map<String, CheckInEntity> map = 
				ofy(client).load().type(CheckInEntity.class).ids(checkInKeys);		
		
		for (int i = 0; i < memberIds.size(); i++) {
			String checkinKey = checkInKeys.get(i);
			
			if (map.containsKey(checkinKey)) {
				CheckInEntity checkInEntity = map.get(checkinKey);
				
				if (checkInEntity != null) {
					CheckInRecord existing = 
							checkInEntity.toProp().getCheckInRecord(sessionDateYYYYMMDD);
					if (existing.isCheckin) {
						CheckInStatusProp checkInStatusProp = result.get(i);
						checkInStatusProp.checkedIn = true;
						checkInStatusProp.userFriendlyMessage = "Checked in by " + 
								existing.login + " " +
								Utils.getDateDiff(existing.timestamp.getTime()/1000, new Date().getTime()/1000) + " ago";
					}					
				}
			}			
		}
		
		return result;		
	}
	
	public static int checkin(final String client, final long memberId, final long programId, final int sessionDateYYYYMMDD, 
			final int batchNo, final String login) {
		
		ensureValidCheckInInputs(client, memberId, programId, sessionDateYYYYMMDD, batchNo, login);
		
		final String key = getCheckInKey(programId, memberId);
		
		CheckInProp checkInProp = ofy(client).transact(new Work<CheckInProp>() {
			
			@Override
			public CheckInProp run() {
				CheckInEntity checkInEntity = ofy(client).load().type(CheckInEntity.class).id(key).get();
				
				if (null == checkInEntity) {
					checkInEntity = new CheckInEntity();
					checkInEntity.key = key;
					checkInEntity.programId = programId;
					checkInEntity.memberId = memberId;						
				} else {			
					CheckInRecord existing = checkInEntity.toProp().getCheckInRecord(sessionDateYYYYMMDD);
					if ((null != existing) && (existing.isCheckin)) {
						String errMessage = "Already checked in by " + existing.login 
								+ " " + Utils.getDateDiff(existing.timestamp.getTime()/1000, 
										new Date().getTime()/1000) + " ago";
						throw new APIException(Status.ERROR_RESOURCE_ALREADY_EXISTS, errMessage);
					}						
				}
				
				CheckInRecord record = new CheckInRecord();
				record.batchNo = batchNo;
				record.isCheckin = true;
				record.login = login;
				record.sessionDateYYYYMMDD = sessionDateYYYYMMDD;
				record.timestamp = new Date();
				
				checkInEntity.checkInRecords.add(record);		
				ofy(client).save().entity(checkInEntity).now();
				
				return checkInEntity.toProp();				
			}
		});
				
		ProgramProp programProp = Program.safeGet(client, programId).toProp();
		if (checkInProp.getNumCheckins() == programProp.getNumSessions())
			Member.addOrDeleteProgram(client, memberId, programId, true, login);
		else {
			Member.addOrDeleteProgram(client, memberId, programId, false, login);
		}
			
		String counterKey = getCounterKey(programId, sessionDateYYYYMMDD, batchNo);
		long numCheckIns = Counter.incrementAndGetCurrentCount(client, CounterType.CHECKIN, counterKey);
		return (int)numCheckIns;		
	}
	
	public static int checkout(final String client, final long memberId, final long programId, 
			final int sessionDateYYYYMMDD, final String login) {
		
		ensureValidCheckInInputs(client, memberId, programId, sessionDateYYYYMMDD, 1, login);
		
		final String key = getCheckInKey(programId, memberId);
		
		Integer batchNo = ofy(client).transact(new Work<Integer>() {
			
			@Override
			public Integer run() {
				CheckInEntity checkInEntity = ofy(client).load().type(CheckInEntity.class).id(key).get();
				
				if (null == checkInEntity)
					throw new APIException(Status.ERROR_RESOURCE_DOES_NOT_EXIST, 
							"Member [" + memberId + "] is not checked in");
				
				CheckInRecord existing = checkInEntity.toProp().getCheckInRecord(sessionDateYYYYMMDD);
				if (null == existing)
					throw new APIException(Status.ERROR_RESOURCE_DOES_NOT_EXIST, 
							"Member [" + memberId + "] is not checked in");
				
				if (!existing.isCheckin) {
					String errMessage = "Already checked out by " + existing.login + " "
							+ Utils.getDateDiff(existing.timestamp.getTime()/1000, new Date().getTime()/1000)
							+ " ago";
					throw new APIException(Status.ERROR_RESOURCE_DOES_NOT_EXIST, errMessage);
				}								
				
				CheckInRecord record = new CheckInRecord();
				record.batchNo = existing.batchNo;
				record.isCheckin = false;
				record.login = login;
				record.sessionDateYYYYMMDD = sessionDateYYYYMMDD;
				record.timestamp = new Date();
									
				checkInEntity.checkInRecords.add(record);		
				ofy(client).save().entity(checkInEntity);
				
				return existing.batchNo;
			}
		});
		
		//remove the program from member
		Member.addOrDeleteProgram(client, memberId, programId, false, login);
			
		String counterKey = getCounterKey(programId, sessionDateYYYYMMDD, batchNo);
		long numCheckIns = Counter.incrementAndGetCurrentCount(client, CounterType.CHECKIN, counterKey, -1);
		return (int)numCheckIns;		
	}
	
	public static int getNumCheckins(String client, long programId, int sessionDateYYYYMMDD, 
			int batchNo) {
		
		ensureValidCheckInInputs(client, programId, sessionDateYYYYMMDD, batchNo);
		String counterKey = getCounterKey(programId, sessionDateYYYYMMDD, batchNo);
		
		long numCheckIns = Counter.getCount(client, CounterType.CHECKIN, counterKey);
		return (int)numCheckIns;		
	}
	
	private static String getCounterKey(long programId, long sessionDateYYYYMMDD, int batchNo) {
		return programId + "_" + sessionDateYYYYMMDD + "_" + batchNo;
	}
	
	private static String getCheckInKey(long programId, long memberId) {
		return programId + "_" + memberId;
	}
	
	private static void ensureValidCheckInInputs(String client, long memberId, long programId, 
			int sessionDateYYYYMMDD, int batchNo, String login) {
		Client.ensureValidClient(client);
		ProgramProp programProp = Program.safeGet(client, programId).toProp();
		
		User.ensureAccess(client, login, ResourceType.GROUP, programProp.groupProp.groupId, Action.WRITE);
		
		Member.safeGet(client, memberId, login);
		
		Utils.ensureFormatYYYYMMDD(sessionDateYYYYMMDD);
		
		if ((sessionDateYYYYMMDD < programProp.startYYYYMMDD) || (sessionDateYYYYMMDD > programProp.endYYYYMMDD))
			Utils.throwIncorrectSpecException("session date [" + sessionDateYYYYMMDD + 
					"] should be between program start [" + programProp.startYYYYMMDD + 
					"] and end date [" + programProp.endYYYYMMDD + "]");
		
		if (batchNo > programProp.numBatches)
			Utils.throwIncorrectSpecException("Batch number [" + batchNo + 
					"] is greater than total number of batches [" + programProp.numBatches + "]");
	}
	
	private static void ensureValidCheckInInputs(String client, long programId, 
			int sessionDateYYYYMMDD, int batchNo) {
		Client.ensureValidClient(client);
		ProgramProp programProp = Program.safeGet(client, programId).toProp();
		
				
		Utils.ensureFormatYYYYMMDD(sessionDateYYYYMMDD);
		
		if ((sessionDateYYYYMMDD < programProp.startYYYYMMDD) || (sessionDateYYYYMMDD > programProp.endYYYYMMDD))
			Utils.throwIncorrectSpecException("session date [" + sessionDateYYYYMMDD + 
					"] should be between program start [" + programProp.startYYYYMMDD + 
					"] and end date [" + programProp.endYYYYMMDD + "]");
		
		if (batchNo > programProp.numBatches)
			Utils.throwIncorrectSpecException("Batch number [" + batchNo + 
					"] is greater than total number of batches [" + programProp.numBatches + "]");
	}
}
